﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace PacketRush.Core
{
    /// <summary>
    /// A client-side networking provider that uses UDP for reliable and unreliable communication.
    /// </summary>
    public class ReliableUdpClientNetworkingProvider : ReliableUdpNetworkingProvider, IClientNetworkingProvider
    {
        /// <summary>
        /// Occurs during an <see cref="PacketRush.Core.ReliableUdpClientNetworkingProvider.Update"/> raised before
        /// any logic is performed on the connection.
        /// </summary>
        public event EventHandler<ReliableUdpEventArgs> BeforeUpdate;

        /// <summary>
        /// Raises the <see cref="PacketRush.Core.ReliableUdpClientNetworkingProvider.BeforeUpdate"/> event.
        /// </summary>
        protected void OnBeforeUpdate(ReliableUdpConnection conn)
        {
            if (BeforeUpdate != null)
            {
                BeforeUpdate(this, new ReliableUdpEventArgs(conn));
            }
        }

        private EndPoint _RemoteEndPoint;
        private EndPoint _LocalEndPoint;

        private ReliableUdpConnection _Connection;

        private ReliableUdpClientSettings _Settings;

        /// <summary>
        /// Time measured from when <see cref="PacketRush.Core.ReliableUdpClientNetworkingProvider.Connect"/> was called.
        /// </summary>
        /// <remarks>
        /// Time measured from when <see cref="PacketRush.Core.ReliableUdpClientNetworkingProvider.Connect"/> was called, resets to 
        /// zero on disconnect.
        /// </remarks>
        public override NetTime RunningTime { get; set; }

        /// <summary>
        /// Creates an instance.
        /// </summary>
        /// <param name="localAddress">The local ip address.</param>
        /// <param name="localPort">The local port, use zero to choose any available port.</param>
        public ReliableUdpClientNetworkingProvider(IPAddress localAddress, int localPort)
            : this(new ReliableUdpClientSettings(), localAddress, localPort)
        {
        }

        /// <summary>
        /// Creates an instance with settings.
        /// </summary>
        /// <param name="settings">Settings to create the instance with.</param>
        /// <param name="localAddress">The local ip address.</param>
        /// <param name="localPort">The local port, use zero to choose any available port.</param>
        public ReliableUdpClientNetworkingProvider(ReliableUdpClientSettings settings, IPAddress localAddress, int localPort)
            : base(settings)
        {
            _Settings = settings;
            _LocalEndPoint = new IPEndPoint(localAddress, localPort);
        }
        /// <summary>
        /// Connect to a server.
        /// </summary>
        /// <param name="remoteEndPoint">The remote end point of the server to connect to.</param>
        public void Connect(System.Net.EndPoint remoteEndPoint)
        {
            _RemoteEndPoint = remoteEndPoint;
            Socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            Socket.Bind(_LocalEndPoint);

            // Make sure we have the right end point
            _LocalEndPoint = Socket.LocalEndPoint;

            _Connection = new ReliableUdpConnection(_RemoteEndPoint, _LocalEndPoint);
            _Connection.Connected = true;
            SendConnectionPacket();
        }

        private void SendConnectionPacket()
        {
            PacketWriter.Reset();
            byte[] connectionPacket = PacketWriter.ToRawPacket(ReliableUdpSystemPacketTypes.ConnectionRequest, PacketFlags.Reliable);
            Packet.WriteSequenceIndex(_Connection.CreateNextPacketSequenceIndex(), connectionPacket); // Assign this packet a sequence id
            SendReliable(_Connection, connectionPacket);
        }
        /// <summary>
        /// Accept the connection.
        /// </summary>
        /// <returns>Returns the connection.</returns>
        public INetConnection AcceptConnection()
        {
            if (_Connection != null && _Connection.Connected)
            {
                return _Connection;
            }

            return null;
        }

        /// <summary>
        /// Called during a <see cref="PacketRush.Core.ReliableUdpNetworkingProvider.Receive"/> when a packet is received.
        /// </summary>
        /// <param name="rawPacket">The packet received.</param>
        /// <param name="serviceId">The service id of the service raw packet is destined for.</param>
        /// <param name="connection">The connection the packet was received from.</param>
        protected override void ProcessReceivedPacket(byte[] rawPacket, int serviceId, ReliableUdpConnection connection)
        {
            // Only pass up packets that fall outside
            if (serviceId > ReliableUdpSystemPacketTypes.Min ||
                serviceId < ReliableUdpSystemPacketTypes.Max)
            {
                ReceivePacketAction(rawPacket, connection);
                return;
            }
        }

        /// <summary>
        /// Called during a <see cref="PacketRush.Core.ReliableUdpNetworkingProvider.Receive"/> when an 
        /// acknowledgement packet is received.
        /// </summary>
        /// <param name="rawPacket">The packet received.</param>
        /// <param name="serviceId">The service id of the service raw packet is destined for.</param>
        /// <param name="connection">The connection the packet was received from.</param>
        protected override void ProcessReceivedAckPacket(byte[] rawPacket, int serviceId, ReliableUdpConnection connection)
        {

        }

        /// <summary>
        /// Performs additional networking logic.
        /// </summary>
        /// <remarks>
        /// <c>Update</c> enumerates packets waiting for acknowledgement, and for each packet it checks
        /// if the time the packet was sent is greater than the <see cref="PacketRush.Core.ReliableUdpSettings.PacketResendRate"/>,
        /// if it is then the packet is resent.
        /// </remarks>
        public override void Update()
        {
            long now = RunningTime.ElapsedMilliseconds;

            OnBeforeUpdate(_Connection);

            foreach (uint key in _Connection.AckWaitingList.Keys)
            {
                AckWaitingPacket packet = _Connection.AckWaitingList[key];

                if ((now - packet.TimeSent) > _Settings.PacketResendRate)
                {
                    // Resend this packet
                    Socket.SendTo(packet.RawPacket, _Connection.RemoteEndPoint);
                    packet.TimeSent = now;
                    packet.Tries++;

                    // If we have reached our limit then disconnect this connection
                    if (packet.Tries > _Settings.MaxPacketResends) _Connection.Connected = false;
                }
            }
        }

        /// <summary>
        /// Used by receive to lookup a connection by endpoint.
        /// </summary>
        /// <remarks>
        /// This method was to share implementation of <c>Receive</c> between client-side and server-side
        /// networking providers, parameters have no effect here since <c>ReliableUdpClientNetworkingProvider</c>
        /// only maintains one connection, the arguments are used for the server-side implementation.
        /// </remarks>
        /// <param name="endPoint">The endpoint to lookup the connection by.</param>
        /// <param name="serviceId">The service id of the packet received.</param>
        /// <returns>A connection.</returns>
        protected override ReliableUdpConnection LookupConnection(System.Net.EndPoint endPoint, int serviceId)
        {
            return _Connection;
        }

        /// <summary>
        /// Disconnect this provider from the network.
        /// </summary>
        public override void Disconnect()
        {
            if (_Connection != null)
            {
                // Fire off a packet to notify the server that this client explicitly disconnected
                PacketWriter.Reset();
                byte[] disconnPacket = PacketWriter.ToRawPacket(ReliableUdpSystemPacketTypes.DisconnectionNotification, PacketFlags.None);
                SendUnreliable(_Connection, disconnPacket);
                _Connection.Connected = false;
                _Connection = null;
            }

            if (Socket != null)
            {
                Socket.Close();
                Socket = null;
            }
        }
    }
}
